import utf8d from "@3-/utf8/utf8d.js"

const v0 = (f) => (buf) => f(buf, 0)[0],
	skipU32 = (buffer, pos) => {
		while (pos < buffer.length) {
			if ((buffer[pos++] & 0x80) === 0) {
				return pos
			}
		}
		return pos
	},
	skipLen = (buffer, pos) => {
		const [len, newPos] = _decodeInt32(buffer, pos)
		return newPos + len
	},
	get = (byteLength, methodName) => (buf, pos) => {
		const view = new DataView(buf.buffer, buf.byteOffset + pos, byteLength)
		return [view[methodName](0, true), pos + byteLength]
	},
	liDecode = (decode) => (buffer) => {
		const li = [],
			end = buffer.length
		let pos = 0
		while (pos < end) {
			const [value, nextPos] = decode(buffer, pos)
			li.push(value)
			pos = nextPos
		}
		return li
	},
	readTag = (buffer) => {
		if (buffer.length > 0) {
			let tagValue = 0
			let position = 0
			let shift = 0

			while (true) {
				if (position >= buffer.length) {
					return
				}

				const byte = buffer[position]

				const payload = byte & 0x7f

				tagValue |= payload << shift

				if ((byte & 0x80) === 0) {
					break
				}

				position++
				shift += 7

				// 安全检查：一个32位的Tag不应该超过5个字节
				if (shift > 28) {
					return
				}
			}

			return [
				tagValue & 7, // wireType
				tagValue >>> 3, // fieldNumber
				position + 1, // bytesRead
			]
		}
	},
	unpack = (buffer) => {
		const result = []
		let pos = 0, tagInfo;
		while (pos < buffer.length) {
			tagInfo = readTag(buffer.subarray(pos))
			if (!tagInfo) {
				break
			}
			const [wireType, fieldNumber, tagBytes] = tagInfo
			pos += tagBytes

			let dataEnd
			let dataStart = pos
			switch (wireType) {
				case 0: // Varint
					dataEnd = skipU32(buffer, pos)
					break
				case 1: // 64-bit
					dataEnd = pos + 8
					break
				case 2: // Length-delimited
					const [len, newPos] = _decodeInt32(buffer, pos)
					dataStart = newPos
					dataEnd = newPos + len
					break
				case 5: // 32-bit
					dataEnd = pos + 4
					break
				default:
					return result
			}
			result.push([fieldNumber - 1, buffer.subarray(dataStart, dataEnd)])
			pos = dataEnd
		}
		return result
	},
	_decodeDouble = get(8, "getFloat64"),
	_decodeFloat = get(4, "getFloat32"),
	_decodeInt32 = (buf, pos) => {
		let val = 0
		let shift = 0
		let byte
		do {
			byte = buf[pos++]
			val |= (byte & 0x7f) << shift
			shift += 7
		} while (byte & 0x80)
		return [val, pos]
	},
	_decodeUint32 = _decodeInt32,
	_decodeInt64 = _decodeInt32,
	_decodeUint64 = _decodeInt32,
	_decodeSint32 = (buf, pos) => {
		const [val, newPos] = _decodeInt32(buf, pos)
		return [(val >>> 1) ^ -(val & 1), newPos]
	},
	_decodeSint64 = _decodeSint32,
	_decodeFixed32 = get(4, "getUint32"),
	_decodeSfixed32 = get(4, "getInt32"),
	_decodeFixed64 = get(8, "getBigUint64"),
	_decodeSfixed64 = get(8, "getBigInt64"),
	_decodeBool = (buf, pos) => {
		const [val, newPos] = _decodeInt32(buf, pos)
		return [!!val, newPos]
	}

export const decodeDouble = v0(_decodeDouble),
	decodeFloat = v0(_decodeFloat),
	decodeInt32 = v0(_decodeInt32),
	decodeUint32 = decodeInt32,
	decodeInt64 = decodeInt32,
	decodeUint64 = decodeInt64,
	decodeSint32 = v0(_decodeSint32),
	decodeSint64 = decodeSint32,
	decodeFixed32 = v0(_decodeFixed32),
	decodeSfixed32 = v0(_decodeSfixed32),
	decodeFixed64 = v0(_decodeFixed64),
	decodeSfixed64 = v0(_decodeSfixed64),
	decodeBool = v0(_decodeBool),
	decodeString = utf8d,
	decodeBytes = (buf) => buf,
	decodeInt32Li = liDecode(_decodeInt32),
	decodeUint32Li = liDecode(_decodeInt32),
	decodeInt64Li = liDecode(_decodeInt64),
	decodeUint64Li = liDecode(_decodeInt64),
	decodeSint32Li = liDecode(_decodeSint32),
	decodeSint64Li = liDecode(_decodeSint64),
	decodeFixed32Li = liDecode(_decodeFixed32),
	decodeSfixed32Li = liDecode(_decodeSfixed32),
	decodeFixed64Li = liDecode(_decodeFixed64),
	decodeSfixed64Li = liDecode(_decodeSfixed64),
	decodeBoolLi = liDecode(_decodeBool),
	decodeDoubleLi = liDecode(_decodeDouble),
	decodeFloatLi = liDecode(_decodeFloat),
	BIN = new Uint8Array(),
	def = (type_li, default_li, nopack_repeat) => {
		nopack_repeat = new Set(nopack_repeat)
		return (buffer) => {
			const r = structuredClone(default_li)
			buffer &&
				unpack(buffer).forEach(([pos, buf]) => {
					const f = type_li[pos]
					if (f) {
						const t = f(buf)
						if (nopack_repeat.has(pos)) {
							r[pos].push(t)
						} else {
							r[pos] = t
						}
					}
				})
			return r
		}
	}
